package com.zero.manage.web.jwt;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import io.gitee.zerowsh.config.RedisUtil;
import io.gitee.zerowsh.constant.ToolConstant;
import io.gitee.zerowsh.enums.ResultEnums;
import io.gitee.zerowsh.util.JwtUtil;
import io.gitee.zerowsh.util.PublicResult;
import io.gitee.zerowsh.util.ZeroException;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Objects;

/**
 * JWT过滤
 *
 * @author zero
 */
@Log4j2
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
     * 这里我们详细说明下为什么最终返回的都是true，即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false，请求会被直接拦截，用户看不到任何东西
     * 所以我们在这里返回true，Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问，我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点，就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法)，但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 查看当前Header中是否携带Authorization属性(Token)，有的话就进行登录认证授权
        if (this.isLoginAttempt(request, response)) {
            try {
                boolean verify = JwtUtil.verify(this.getAuthzHeader(request));
                if (!verify) {
                    //该异常为JWT的AccessToken已过期，判断RefreshToken未过期就进行AccessToken刷新
                    if (this.refreshToken(request, response)) {
                        return true;
                    } else {
                        this.response401(response, PublicResult.resultEnums(ResultEnums.TOKEN_INVALID_0));
                        return false;
                    }
                }
                // 进行Shiro的登录UserRealm
                this.executeLogin(request, response);
            } catch (Exception e) {
                // 认证出现异常，传递错误信息msg
                PublicResult result = PublicResult.resultEnums(ResultEnums.TOKEN_AUTHENTICATION_0, e.getMessage());
                if (e instanceof ZeroException) {
                    //自定义Jwt异常处理
                    result = JSONUtil.toBean(e.getMessage(), PublicResult.class);
                }
                // Token认证失败直接返回Response信息
                this.response401(response, result);
                return false;
            }
        } else {
//            // 没有携带Token
//            HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
//            // 获取当前请求类型
//            String httpMethod = httpServletRequest.getMethod();
//            // 获取当前请求URI
//            String requestURI = httpServletRequest.getRequestURI();
//            log.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
            this.response401(response, PublicResult.resultEnums(ResultEnums.UN_LOGIN_0));
            return false;
        }
        return true;
    }

    /**
     * 这里我们详细说明下为什么重写
     * 可以对比父类方法，只是将executeLogin方法调用去除了
     * 如果没有去除将会循环调用doGetAuthenticationInfo方法
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        this.sendChallenge(request, response);
        return false;
    }

    /**
     * 检测Header里面是否包含Authorization字段，有就进行Token登录认证授权
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        return StrUtil.isNotBlank(this.getAuthzHeader(request));
    }

    /**
     * 从请求中获取token信息
     *
     * @param request
     * @return
     */
    @Override
    protected String getAuthzHeader(ServletRequest request) {
        // 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
        String token = super.getAuthzHeader(request);
        if (null == token) {
            token = request.getParameter(ToolConstant.ACCESS_TOKEN_NAME);
        }
        return token;
    }

    /**
     * 进行AccessToken登录认证授权
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        JwtToken token = new JwtToken(this.getAuthzHeader(request));
        // 提交给UserRealm进行认证，如果错误他会抛出异常并被捕获
        this.getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功，返回true
        return true;
    }

//    /**
//     * 对跨域提供支持
//     */
//    @Override
//    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
//        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
//        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
////        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
////        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
////        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
//        // 跨域时会首先发送一个OPTIONS请求，这里我们给OPTIONS请求直接返回正常状态
//        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
//            httpServletResponse.setStatus(ResultEnums.OK.getKey());
//            return false;
//        }
//        return super.preHandle(request, response);
//    }

    /**
     * 此处为AccessToken刷新，进行判断RefreshToken是否过期，未过期就返回新的AccessToken且继续正常访问
     */
    private boolean refreshToken(ServletRequest request, ServletResponse response) {
        // 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
        String token = this.getAuthzHeader(request);
        // 获取当前Token的帐号信息
        String username = JwtUtil.getClaim(token, ToolConstant.MAME);
        // 获取RefreshToken的时间戳
        String refreshTokenTime = RedisUtil.getStr(String.format(ToolConstant.REFRESH_ACCESS_TOKEN_PREFIX, username));
        //解析token存入时间
        String claim = JwtUtil.getClaim(token, ToolConstant.CURRENT_TIME_MILLIS);
        synchronized (this) {
            // 当前AccessToken中的时间戳，与RefreshToken的时间戳对比，如果当前时间戳一致，进行AccessToken刷新
            if (Objects.equals(refreshTokenTime, claim)
                    && Objects.equals(token, RedisUtil.getStr(String.format(ToolConstant.ACCESS_TOKEN_PREFIX, username)))) {
                //从新生成token
                String nowToken = JwtUtil.buildTokenInfo(username);
                //获取该用户存入redis中的名称
                String format = String.format(ToolConstant.ACCESS_TOKEN_PREFIX, username);

                RedisUtil.setExSeconds(String.format(ToolConstant.CACHE_TOKEN_PREFIX, username), token, ToolConstant.CACHE_TOKEN_EXPIRE_TIME);
                RedisUtil.setExSeconds(format, nowToken, ToolConstant.REDIS_EXPIRE_TIME);
                // 将新刷新的AccessToken再次进行Shiro的登录
                JwtToken jwtToken = new JwtToken(nowToken);
                // 提交给UserRealm进行认证，如果错误他会抛出异常并被捕获，如果没有抛出异常则代表登入成功，返回true
                this.getSubject(request, response).login(jwtToken);
                // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
                HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
                httpServletResponse.setHeader(ToolConstant.ACCESS_TOKEN_NAME, nowToken);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", ToolConstant.ACCESS_TOKEN_NAME);
                log.info("刷新token");
                return true;
            } else {
                //如果不一致，判断是否有
                String str = RedisUtil.getStr(String.format(ToolConstant.CACHE_TOKEN_PREFIX, username));
                if (Objects.equals(str, token)) {
                    JwtToken jwtToken = new JwtToken(RedisUtil.getStr(String.format(ToolConstant.ACCESS_TOKEN_PREFIX, username)));
                    // 提交给UserRealm进行认证，如果错误他会抛出异常并被捕获，如果没有抛出异常则代表登入成功，返回true
                    this.getSubject(request, response).login(jwtToken);
                    log.info("使用缓存token");
                    return true;
                }
                return false;
            }
        }
    }

    /**
     * 无需转发，直接返回Response信息
     */
    private void response401(ServletResponse response, PublicResult result) {
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setStatus(ResultEnums.UNAUTHORIZED.getKey());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        try (PrintWriter out = httpServletResponse.getWriter()) {
            out.append(JSONUtil.toJsonStr(result));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
